# -*- coding: utf-8 -*-
"""
轮动策略框架 | 邢不行 | 2024分享会
author: 邢不行
微信: xbx6660
"""
import pandas as pd
import numpy as np
import itertools
from glob import glob
import os


# ===读取各个资金曲线数据
def read_equity(path, strategy_dict, hold_period, offset, benchmark):
    """
    读取资金曲线数据
    :param path: 资金曲线的路径
    :param strategy_dict: 策略文件加下配置的子策略及其资金曲线的字典
    :param hold_period: 持仓周期
    :param offset: offset偏移
    :param benchmark: 基准数据
    :return:
        df，返回整理好的各个资金曲线的数据
    """
    base_name = os.path.basename(path)  # 取出路径的基础文件名，即资金曲线的文件名
    equity_name = base_name.split('_')[0]  # 解析资金曲线文件名，是由哪个策略跑出的资金曲线
    save_equity_cols = strategy_dict[f'Strategy_{equity_name}']  # 根据 资金曲线文件名 取出对应的参与轮动的资金曲线
    save_equity_cols_byclose = [col + '_byclose' for col in save_equity_cols]  # 根据指定的资金曲线取出对应的以收盘价计算的资金曲线

    # 读取equity文件
    equity = pd.read_csv(path, encoding='gbk', parse_dates=['candle_begin_time'])
    if len(equity) == 0:
        print('资金曲线为空，请检查资金曲线文件')
        exit()

    not_exist_cols = [col for col in save_equity_cols if col not in equity.columns]
    if not_exist_cols:
        print(not_exist_cols, '列不存在，请检查策略配置。')
        exit()
    equity = equity[['candle_begin_time', '选币'] + save_equity_cols + save_equity_cols_byclose]  # 保留部分列

    # 修改选币列的格式，生成三列，分别时多头选币、空头选币、多空选币
    equity['多空资金曲线_选币'] = equity['选币'].apply(lambda x: [symbol.strip() for symbol in x.split(' ')][: -1])  # 如果当周期为空仓，则选币为空[]
    equity['多头资金曲线_选币'] = equity['多空资金曲线_选币'].apply(lambda x: [symbol for symbol in x if symbol.endswith(',1)')])
    equity['空头资金曲线_选币'] = equity['多空资金曲线_选币'].apply(lambda x: [symbol for symbol in x if symbol.endswith(',-1)')])

    # ===resample至日线数据
    # 如果不是1H持仓周期的，存在不同的open、high、low、close；如果是1H持仓周期，则open=high=low=close
    resample_cols = {'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last', '选币': 'last'}

    # 创建空列表用来保存整理好的df
    equity_list = []
    for equity_col in save_equity_cols:  # 遍历指定的资金曲线
        # 取出对应的资金曲线数据、对应的选币数据、对应的以close计算的资金曲线数据
        _equity = equity[['candle_begin_time', equity_col, equity_col + '_byclose', equity_col + '_选币']]
        _equity.rename(columns={equity_col + '_选币': '选币'}, inplace=True)

        # 开始时间
        _start_date = _equity['candle_begin_time'].iloc[0]

        # 把资金曲线列复制成open、high、low、close。为了回测与实盘对应，使用close计算得到的资金曲线为基础，生成ohlc，计算因子
        _equity['open'] = _equity[equity_col + '_byclose']
        _equity['high'] = _equity[equity_col + '_byclose']
        _equity['low'] = _equity[equity_col + '_byclose']
        _equity['close'] = _equity[equity_col + '_byclose']

        # 计算每小时的涨跌幅并resample为list格式，计算涨跌幅仍然使用的是avg_price计算得到的资金曲线
        _equity['涨跌幅'] = _equity[equity_col].pct_change()
        _equity['涨跌幅'].fillna(_equity[equity_col] - 1, inplace=True)

        # ===与benchmark合并
        _equity = pd.merge(left=benchmark, right=_equity, on='candle_begin_time', how='left')
        _equity.sort_values(by='candle_begin_time', inplace=True)

        # resample数据
        period_equity = _equity.resample(hold_period, on='candle_begin_time', base=offset).agg(resample_cols)
        period_equity.reset_index(inplace=True)

        # resample涨跌幅为每小时涨跌幅
        period_equity['每小时涨跌幅'] = _equity.resample(hold_period, on='candle_begin_time', base=offset)['涨跌幅'].apply(lambda x: list(x)).values

        # 根据开始时间截取之后的数据
        period_equity = period_equity[period_equity['candle_begin_time'] >= _start_date]

        # 指定策略名；格式为 策略名 + 多空/多头/空头
        period_equity['symbol'] = equity_name + '_' + equity_col.split('资金曲线')[0]

        # =把生成好的df添加到列表中
        equity_list.append(period_equity)

    # 合并并整理数据
    equity = pd.concat(equity_list, axis=0)
    equity = equity.sort_values(['candle_begin_time', 'symbol']).reset_index(drop=True)

    return equity


def calc_factors_for_filename(df, factor_list, filename='', param_list=[]):
    """
    使用文件夹下的因子脚本进行计算因子
    :param df: 原始k线数据
    :param factor_list: 需要计算的因子列表
    :param filename: 指定因子文件夹名称
    :param param_list: 因子参数
    :return:
    """
    column_list = []
    # 根据config中设置的因子列表，逐个计算每个因子的数据
    for factor in factor_list:
        factor = factor.split('.')[0]
        _cls = __import__('%s.%s' % (filename, factor), fromlist=('',))
        # 兼容get_parameter
        if 'get_parameter' in _cls.__dict__:  # 如果存在get_parameter，直接覆盖config中配置的factor_param_list
            _param_list = getattr(_cls, 'get_parameter')()
        else:  # 如果不存在get_parameter，直接用config中配置的factor_param_list
            _param_list = param_list.copy()
        # 遍历参数，计算每个参数对应的因子值
        for n in _param_list:
            # 构建因子列名
            factor_name = f'{factor}_{str(n)}'
            # 因子已经计算过，则跳过
            if factor_name in df.columns:
                continue
            # 计算因子
            df = getattr(_cls, 'signal')(df, n, factor_name)
            # 为了跟实盘保持一致，所有因子信息在下个周期生效
            df[factor_name] = df[factor_name].shift(1)
            # 保存因子列名
            column_list.append(factor_name)

    return df, column_list


def calc_factors(all_equity, symbol, factor_names, factor_params):
    """
    计算因子
    :param all_equity: 所有资金曲线的数据
    :param symbol: 资金曲线名
    :param factor_names: 计算的因子
    :param factor_params: 计算因子的参数
    :return:
        返回每个资金曲线计算好因子之后的df
    """
    # 取出对应资金曲线的数据
    df = all_equity[all_equity['symbol'] == symbol].reset_index(drop=True)

    # 计算涨跌幅
    df['涨跌幅'] = df['close'].pct_change()
    df['涨跌幅'].fillna(df['close'] / df['open'] - 1, inplace=True)

    # =根据净值数据计算因子
    df, _ = calc_factors_for_filename(df, factor_names, filename='factors', param_list=factor_params)

    return df


def select_strategy(df, factor_list, select_num, **kwargs):
    """
    根据计算出的因子选择策略
    :param df: 所有资金曲线的数据，已经计算好因子
    :param factor_list: 因子列表
    :param select_num: 选策略的数量
    :param kwargs:
    :return:
        返回根据因子选择出来的策略
    """

    # 接受外部回测的因子列表，这里主要是去适配`4_遍历选币参数.py`
    external_list = kwargs.get('external_list', [])
    if external_list:  # 如果存在外部回测因子列表，则使用外部因子列表
        _factor_list = external_list
    else:  # 如过不存在外部回测因子列表，默认使用当前策略的因子列表
        _factor_list = factor_list

    # 同保温杯框架，计算得到因子排名值
    df['_rank'] = 0
    for factor_name, if_reverse, parameter_list, weight in _factor_list:
        col_name = f'{factor_name}_{str(parameter_list)}'
        # 计算单个因子的排名
        df[col_name + '_rank'] = df.groupby('candle_begin_time')[col_name].rank(ascending=if_reverse, method='min')
        # 将因子按照权重累加
        df['_rank'] += (df[col_name + '_rank'] * weight)
    df['rank'] = df.groupby('candle_begin_time')['_rank'].rank(ascending=True, method='first')

    # 选择排名小于select_num的对应的策略
    df = df[df['rank'] <= select_num].reset_index(drop=True)
    if df.empty:
        print('没有选出轮动的资金曲线，请检查配置')
        exit()

    return df


def cal_rot_net_value(df, hold_period, select_num, swap_c_rate, spot_c_rate):
    """
        根据轮动出来的资金曲线，计算轮动的资金曲线
    :param df: 选择策略之后的df
    :param hold_period: 持仓周期
    :param select_num: 选策略数量
    :param swap_c_rate: 合约手续费
    :param spot_c_rate: 现货手续费
    :return:
        返回计算出来的轮动资金曲线
    """
    # 根据选币结果得到选币数量
    df['选币'].fillna('空仓', inplace=True)
    df['选币'] = df['选币'].apply(lambda x: [symbol.strip() for symbol in x.split(' ')][: -1] if x == '空仓' else x)
    df['选币数量'] = df['选币'].apply(len)

    # 整理每周期的数据
    df['symbol'] += ' '
    group = df.groupby('candle_begin_time')
    select_stg = pd.DataFrame()
    select_stg['策略名称'] = group['symbol'].sum()
    select_stg['选币'] = group['选币'].sum()
    select_stg['策略数量'] = group['symbol'].size()
    select_stg['选币数量'] = group['选币数量'].apply(lambda x: list(x)).values

    # 计算每小时资金曲线
    select_stg['每小时资金曲线'] = group['每小时涨跌幅'].apply(lambda x: np.cumprod(np.array(list(x)) + 1, axis=1).mean(axis=0))
    # 计算每小时涨跌幅
    select_stg['每小时涨跌幅'] = select_stg['每小时资金曲线'].apply(lambda x: list(pd.DataFrame([1] + list(x)).pct_change()[0].iloc[1:]))
    del select_stg['每小时资金曲线']
    select_stg.reset_index(inplace=True)

    # =====计算开平仓比例
    # ===生成每个周期每个币种的资金去占比
    # 遍历选币数量，生成每个策略中每个币种的资金占比
    for num in range(select_num):
        if num == 0:
            select_stg['占比'] = select_stg['选币数量'].apply(lambda x: (x[num] * [1 / select_num / x[num]]) if x[num] != 0 else [0])
        else:
            select_stg['占比'] += select_stg['选币数量'].apply(lambda x: (x[num] * [1 / select_num / x[num]]) if x[num] != 0 else [])

    def get_ratios(df, date_range):
        """
            计算每个币种的开平仓比例
        :param df: 每个币种每个周期的资金占比数据
        :return:
            每个币种每个时间的资金占比、及开平仓比例
        """
        df = pd.merge(date_range, df, on='candle_begin_time', how='left')
        df['上周期占比'] = df['占比'].shift()
        df['下周期占比'] = df['占比'].shift(-1)
        df.dropna(subset=['选币'], axis=0, inplace=True)

        df['上周期占比'].fillna(0, inplace=True)
        df['下周期占比'].fillna(0, inplace=True)

        df['开仓比例'] = df['占比'] - df['上周期占比']
        df['开仓比例'] = np.where(df['开仓比例'] < 0, 0, df['开仓比例'])

        df['平仓比例'] = df['占比'] - df['下周期占比']
        df['平仓比例'] = np.where(df['平仓比例'] < 0, 0, df['平仓比例'])

        return df

    # ===根据每周期资金占比得到开平仓比例
    # 取出时间、选币两列，拆分选币。每个时间周期都会对应多个选币
    coins = select_stg[['candle_begin_time', '选币']].explode('选币')
    coins['选币'].fillna('', inplace=True)  # 如果该时间周期没有选币，则explode之后会显示为nan值。这边将nan值填充为 空字符串
    coins['占比'] = select_stg['占比'].sum()  # 将资金占比添加到对应的选币中
    coins = coins.groupby(['candle_begin_time', '选币'])['占比'].sum().reset_index()  # 如果选择多个策略，则每个周期的选币可能重复，将重复币种的资金占比加到一起

    date_range = pd.DataFrame(pd.date_range(start=coins['candle_begin_time'].min(), end=coins['candle_begin_time'].max(), freq=hold_period))
    date_range.rename(columns={0: 'candle_begin_time'}, inplace=True)

    coins = coins.groupby('选币').apply(get_ratios, date_range)  # 根据每个币种计算开仓比例和平仓比例
    coins.reset_index(drop=True, inplace=True)
    coins = coins.sort_values(['candle_begin_time', '选币']).reset_index(drop=True)  # 整理数据
    coins['symbol_type'] = coins['选币'].apply(lambda x: x.split(',')[0].split('(')[1] if x != '' else '')  # 从选币中取出币种类型，之后计算手续费 现货和合约 分开计算

    # 通过开平仓比例及币种类型计算合约开仓比例、现货开仓比例
    select_stg['开仓比例'] = coins.groupby('candle_begin_time')['开仓比例'].sum().values
    select_stg['合约开仓比例'] = coins.groupby('candle_begin_time').apply(lambda x: x[x['symbol_type'] == 'swap']['开仓比例'].sum()).values
    select_stg['现货开仓比例'] = coins.groupby('candle_begin_time').apply(lambda x: x[x['symbol_type'] == 'spot']['开仓比例'].sum()).values
    select_stg['平仓比例'] = coins.groupby('candle_begin_time')['平仓比例'].sum().values
    select_stg['合约平仓比例'] = coins.groupby('candle_begin_time').apply(lambda x: x[x['symbol_type'] == 'swap']['平仓比例'].sum()).values
    select_stg['现货平仓比例'] = coins.groupby('candle_begin_time').apply(lambda x: x[x['symbol_type'] == 'spot']['平仓比例'].sum()).values

    # 判断是否换策略，将没有换策略的周期的开平仓比例设为0(已经在子策略资金曲线中计算过一次)
    select_stg['开仓'] = np.where(select_stg['策略名称'] != select_stg['策略名称'].shift(), 1, 0)
    select_stg['平仓'] = np.where(select_stg['策略名称'] != select_stg['策略名称'].shift(-1), 1, 0)
    select_stg.loc[select_stg['开仓'] == 0, ['开仓比例', '合约开仓比例', '现货开仓比例']] = 0
    select_stg.loc[select_stg['平仓'] == 0, ['平仓比例', '合约平仓比例', '现货平仓比例']] = 0

    # 计算手续费
    def cal_exchange_fee(row):
        """
        在资金曲线上加上手续费的计算
        每个点应扣的手续费 =  ∑（手续费率 x 仓位价值）
        :param row: 每一行的数据
        :return:每个点应扣的手续费
        """
        pos_value = np.array(row['仓位价值'])
        # 开仓的手续费 = lvg * rate * 开仓调整比例
        open_fee = row['合约开仓比例'] * swap_c_rate + row['现货开仓比例'] * spot_c_rate
        # 平仓的手续费 = 仓位价值 * rate * 平仓调整比例
        close_fee = row['仓位价值'][-1] * (row['合约平仓比例'] * swap_c_rate + row['合约平仓比例'] * swap_c_rate)

        # 小时级别的持仓长度（1H持仓长度为1，3H持仓长度为3）
        length = len(pos_value)
        if length > 1:
            # 如果周期长度大于2，每个点的手续费为：[开仓手续费，0,0,0,0，……，平仓手续费]（中间的0也可以是0个0）
            exchange_fee = [open_fee] + [0] * (length - 2) + [close_fee]
        else:
            # 如果周期长度等于1，即这根K线又要开仓又要平仓，手续费 = 开仓手续费 + 平仓手续费
            exchange_fee = [open_fee + close_fee]
        # 对手续费求和
        exchange_fee = np.cumsum(np.array(exchange_fee))

        return exchange_fee

    # 通过开平仓比例、手续费率、合约占比计算手续费
    select_stg['仓位价值'] = select_stg.apply(lambda row: np.cumprod(np.array(row['每小时涨跌幅']) + 1), axis=1)
    select_stg['每小时手续费'] = select_stg.apply(lambda rows: cal_exchange_fee(rows), axis=1)

    # 计算每小时资金曲线
    select_stg['每小时资金曲线'] = select_stg.apply(lambda rows: rows['仓位价值'] - rows['每小时手续费'], axis=1)
    # 计算每小时涨跌幅
    select_stg['每小时涨跌幅'] = select_stg['每小时资金曲线'].apply(lambda x: list(pd.DataFrame([1] + list(x)).pct_change()[0].iloc[1:]))
    # 计算周期的涨跌幅
    select_stg['周期涨跌幅'] = select_stg['每小时资金曲线'].apply(lambda x: x[-1] - 1)

    # 使用label数据为之后的统计、分段画图做准备
    select_stg['label'] = select_stg['开仓'].expanding().sum()

    # 整理数据
    select_stg = select_stg[['candle_begin_time', '策略名称', '选币', '策略数量', '周期涨跌幅', '每小时涨跌幅', '每小时资金曲线', 'label']]

    return select_stg


def get_benchmark(start_date, end_date, freq):
    benchmark = pd.DataFrame(pd.date_range(start=start_date, end=end_date, freq=freq))
    benchmark.rename(columns={0: 'candle_begin_time'}, inplace=True)

    return benchmark


def revise_data_length(data, data_len):
    """
    校正数据长度
    原数据过长，则进行切片
    原数据果断，则使用0填充
    :param data: 原数据
    :param data_len: 资金曲线的数据长度
    :return: 校正后的数据
    """
    if len(data) > data_len:
        data = data[0:data_len]
    elif len(data) < data_len:
        data = data.append(pd.Series([0] * (data_len - len(data))))

    return data


# 计算策略评价指标
def strategy_evaluate(equity, net_col='轮动资金曲线', pct_col='轮动涨跌幅'):
    """
    回测评价函数
    :param equity: 资金曲线数据
    :param net_col: 资金曲线列名
    :param pct_col: 周期涨跌幅列名
    :return:
    """
    # ===新建一个dataframe保存回测指标
    results = pd.DataFrame()

    # 将数字转为百分数
    def num_to_pct(value):
        return '%.2f%%' % (value * 100)

    # ===计算累积净值
    results.loc[0, '累积净值'] = round(equity[net_col].iloc[-1], 2)

    # ===计算年化收益
    annual_return = (equity[net_col].iloc[-1]) ** (
            '1 days 00:00:00' / (equity['candle_begin_time'].iloc[-1] - equity['candle_begin_time'].iloc[0]) * 365) - 1
    results.loc[0, '年化收益'] = num_to_pct(annual_return)

    # ===计算最大回撤，最大回撤的含义：《如何通过3行代码计算最大回撤》https://mp.weixin.qq.com/s/Dwt4lkKR_PEnWRprLlvPVw
    # 计算当日之前的资金曲线的最高点
    equity['max2here'] = equity[net_col].expanding().max()
    # 计算到历史最高值到当日的跌幅，drowdwon
    equity['dd2here'] = equity[net_col] / equity['max2here'] - 1
    # 计算最大回撤，以及最大回撤结束时间
    end_date, max_draw_down = tuple(equity.sort_values(by=['dd2here']).iloc[0][['candle_begin_time', 'dd2here']])
    # 计算最大回撤开始时间
    start_date = equity[equity['candle_begin_time'] <= end_date].sort_values(by=net_col, ascending=False).iloc[0]['candle_begin_time']
    # 将无关的变量删除
    # temp.drop(['max2here', 'dd2here'], axis=1, inplace=True)
    results.loc[0, '最大回撤'] = num_to_pct(max_draw_down)
    results.loc[0, '最大回撤开始时间'] = str(start_date)
    results.loc[0, '最大回撤结束时间'] = str(end_date)
    # ===年化收益/回撤比：我个人比较关注的一个指标
    results.loc[0, '年化收益/回撤比'] = round(annual_return / abs(max_draw_down), 2)
    # ===统计每个周期
    results.loc[0, '盈利周期数'] = len(equity.loc[equity[pct_col] > 0])  # 盈利笔数
    results.loc[0, '亏损周期数'] = len(equity.loc[equity[pct_col] <= 0])  # 亏损笔数
    results.loc[0, '胜率'] = num_to_pct(results.loc[0, '盈利周期数'] / len(equity))  # 胜率
    results.loc[0, '每周期平均收益'] = num_to_pct(equity[pct_col].mean())  # 每笔交易平均盈亏
    results.loc[0, '盈亏收益比'] = round(equity.loc[equity[pct_col] > 0][pct_col].mean() / equity.loc[equity[pct_col] <= 0][pct_col].mean() * (-1), 2)  # 盈亏比
    results.loc[0, '单周期最大盈利'] = num_to_pct(equity[pct_col].max())  # 单笔最大盈利
    results.loc[0, '单周期大亏损'] = num_to_pct(equity[pct_col].min())  # 单笔最大亏损

    # ===连续盈利亏损
    results.loc[0, '最大连续盈利周期数'] = max(
        [len(list(v)) for k, v in itertools.groupby(np.where(equity[pct_col] > 0, 1, np.nan))])  # 最大连续盈利次数
    results.loc[0, '最大连续亏损周期数'] = max(
        [len(list(v)) for k, v in itertools.groupby(np.where(equity[pct_col] <= 0, 1, np.nan))])  # 最大连续亏损次数

    # ===其他评价指标
    results.loc[0, '收益率标准差'] = num_to_pct(equity[pct_col].std())

    # ===每年、每月收益率
    temp = equity.copy()
    temp.set_index('candle_begin_time', inplace=True)
    year_return = temp[[pct_col]].resample(rule='A').apply(lambda x: (1 + x).prod() - 1)
    month_return = temp[[pct_col]].resample(rule='M').apply(lambda x: (1 + x).prod() - 1)

    def num2pct(x):
        if str(x) != 'nan':
            return str(round(x * 100, 2)) + '%'
        else:
            return x

    year_return['涨跌幅'] = year_return[pct_col].apply(num2pct)

    # 对每月收益进行处理，做成二维表
    month_return.reset_index(inplace=True)
    month_return['year'] = month_return['candle_begin_time'].dt.year
    month_return['month'] = month_return['candle_begin_time'].dt.month
    month_return.set_index(['year', 'month'], inplace=True)
    del month_return['candle_begin_time']
    month_return_all = month_return[pct_col].unstack()
    month_return_all.loc['mean'] = month_return_all.mean(axis=0)
    month_return_all = month_return_all.apply(lambda x: x.apply(num2pct))

    return results.T, year_return[['涨跌幅']], month_return_all


def get_variable_from_py_file(py_path, var_dict):
    """
    从指定的py文件路径中获取指定的变量
    :param py_path: py文件名路径
    :param var_dict: 参数列表，{参数名:类型}
    :return:
        {'spot_c_rate': 0.0004, 'swap_c_rate': 0.001}
    """
    # 判断文件是否存在
    if os.path.exists(py_path):
        # 逐行读入文件
        with open(py_path, 'r', encoding='utf-8') as file:
            lines = file.readlines()
        # 创建新{}保存结果
        res = {}
        # ===将指定的变量和读取到的字符串一一对应
        for var in var_dict.keys():  # 遍历指定的需要获取参数变量
            for line in lines:  # 遍历读取到的各行字符串
                if line.startswith(var):  # 判断是否存在对应的变量
                    # 如果这行代码有注释，先注释之后的内容
                    if '#' in line:
                        inx = line.find('#')
                        line = line[:inx]
                    # 替换掉字符串中的空格及换行
                    line = line.replace('\n', '').replace(' ', '')
                    # 将字符串以=分隔为两份
                    sub_str = line.split('=')
                    # 如果指定的变量类型是str，读取到的是None，则会返回None
                    if var_dict[var] == str and sub_str[1].replace('\'', '').replace('\"', '') == 'None':
                        res[sub_str[0]] = None
                    elif var_dict[var] == bool:  # 指定的变量类型是bool
                        res[sub_str[0]] = eval(sub_str[1])
                    elif var_dict[var] == str:  # 指定的变量类型是str
                        res[sub_str[0]] = sub_str[1].replace('\'', '').replace('\"', '')
                    elif var_dict[var] == float and '/' in sub_str[1]:  # 如果指定的是float，但是读取到的是包含/的分数
                        molecule = float(sub_str[1].split('/')[0])  # 取出分子
                        denominator = float(sub_str[1].split('/')[1])  # 取出分母
                        res[sub_str[0]] = molecule / denominator  # 计算float类型
                    else:  # 其余类型直接使用指定的类型
                        res[sub_str[0]] = var_dict[var](sub_str[1])
                    break
        return res
    else:  # 如果文件不存在则返回空{}
        print('路径错误，未找到对应的py文件：', py_path)
        return {}


def get_equity_path(base_strategy_dict, base_config_path):
    """
    获取对应策略中指定的hold_period、offset、所有对应的资金曲线文件
    :param base_strategy_dict:
    :param base_config_path:
    :return:
        hold_period、offset、资金曲线列表
    """
    # 获取指定策略文件的所有hold_period和offset
    hold_period_set, offset_set = set(), set()
    if_use_spots = []

    # 遍历每个策略文件名
    for stg_name in base_strategy_dict:
        stg_name += '.py'  # 在策略文件名后加上后缀  .py
        stg_path = os.path.join(base_config_path, 'program/strategy', stg_name)  # 拼接路径取出策略文件的路径

        # 获取策略文件名中的hold_period、offset变量
        stg_params = get_variable_from_py_file(stg_path, {'hold_period': eval, 'offset': int, 'if_use_spot': bool})
        if len(stg_params) == 0:
            print('请检查路径配置及对应的策略文件是否存在。')
            exit()
        _hold_period = stg_params['hold_period']  # 取出hold_period
        _offset = stg_params['offset']  # 取出offset
        _if_use_spot = stg_params['if_use_spot']
        if_use_spots.append(_if_use_spot)

        # 将该策略文件中的hold_period、offset添加到集合中
        hold_period_set.add(_hold_period)
        offset_set.add(_offset)

    # 如果各策略文件存在多个持仓周期或offset，则会退出
    if (len(hold_period_set) > 1) | (len(offset_set) > 1):
        print('存在多个持仓周期或offset，请调整一致。')
        exit()

    # 取出唯一的hold_period和offset
    hold_period = list(hold_period_set)[0]
    offset = list(offset_set)[0]

    # 资金曲线文件路径
    equity_path = os.path.join(base_config_path, 'data/回测结果/')
    # 获取所有的资金曲线文件，只找hold_period和offset对应的资金曲线文件
    all_equity_file = []  # 创建新列表保存资金曲线文件
    for stg in list(base_strategy_dict.keys()):  # 遍历每个策略文件名
        file_name = f'{stg.split("_")[1]}_*_资金曲线_{hold_period}_{offset}.csv'  # 以策略文件名_后的部分开头，之后是SPOT或SWAP，再接着hold_period和offset
        equity_file = glob(equity_path + file_name)  # 将文件夹下所有对应的资金曲线文件找出来
        if len(equity_file) == 0:
            print('没有找到对应策略的资金曲线：', stg)
            print('请修改策略配置或生成对应的资金曲线')
            exit()
        all_equity_file.extend(equity_file)  # 添加到列表中

    return hold_period, offset, if_use_spots, all_equity_file


def display_stat_data(df, info_path, stg_name, hold_period, offset):
    """
        计算得到的资金曲线数据
    :param df:
    :param info_path: 数据保存路径
    :param stg_name: 策略文件名
    :param hold_period: 持仓周期
    :param offset: offset
    :return:
        打印并保存统计数据
    """
    # label代表第几次换仓，通过label取得是否换仓列
    df['是否换仓'] = df['label'].diff()
    df['是否换仓'].fillna(1, inplace=True)

    # ===统计各个策略的选中频率、胜率、平均涨幅
    group = df.groupby('策略名称')
    select_stat = group.size().sort_values(ascending=False).to_frame().rename(columns={0: '选中周期数'})
    select_stat['选中周期比例'] = select_stat['选中周期数'] / select_stat['选中周期数'].sum()
    select_stat['选中次数'] = group.agg({'是否换仓': 'sum'})
    select_stat['选中次数比例'] = select_stat['选中次数'] / df['label'].max()
    select_stat['选中周期胜率'] = group.apply(lambda x: (x['周期涨跌幅'] > 0).sum() / len(x))
    select_stat['选中周期平均涨幅'] = group.apply(lambda x: x['周期涨跌幅'].mean())
    temp_df = df.groupby(['策略名称', 'label']).apply(lambda x: np.prod(x['周期涨跌幅'] + 1) - 1).to_frame()
    temp_df.reset_index(inplace=True)
    select_stat['选中次数胜率'] = temp_df.groupby('策略名称').apply(lambda x: (x[0] > 0).sum() / len(x))
    select_stat['选中次数平均涨幅'] = temp_df.groupby('策略名称').apply(lambda x: x[0].mean())
    df.drop('是否换仓', axis=1, inplace=True)

    # ===统计换仓次数
    times = df.resample('Y', on='candle_begin_time').apply(lambda x: x['label'].max() - x['label'].min())
    times = times.reset_index().rename(columns={'candle_begin_time': '年份', 0: '换仓次数'})
    times['年份'] = times['年份'].apply(lambda x: str(x).split('-')[0] + '年')
    times.loc[0, '换仓次数'] += 1
    times.set_index('年份', inplace=True)
    times.loc['全区间', '换仓次数'] = df['label'].max()

    # ===统计最大、最小换仓天数，分别有多少次、以及对应的时间
    df['candle_begin_time'] = df['candle_begin_time'].apply(lambda x: x.strftime('%Y-%m-%d %H:%M:%S'))
    size = df.groupby('label').size()
    max_size = size.max()
    min_size = size.min()
    max_size_freq = len(size[size == max_size])
    min_size_freq = len(size[size == min_size])
    max_size_times, min_size_times = [], []
    for idx in size[size == max_size].index:
        temp_df = df[df['label'] == idx]
        max_size_times.append([temp_df['candle_begin_time'].min(), temp_df['candle_begin_time'].max()])
    for idx in size[size == min_size].index:
        temp_df = df[df['label'] == idx]
        min_size_times.append([temp_df['candle_begin_time'].min(), temp_df['candle_begin_time'].max()])
    df['candle_begin_time'] = pd.to_datetime(df['candle_begin_time'])

    # 打印统计信息
    print('=====子策略表现=====')
    print(select_stat)
    print('=====换仓次数=====')
    print('各年换仓次数：\n', times)
    print('=====最大/最小持仓周期数=====')
    print('最大持仓周期数', max_size)
    print('最小持仓周期数', min_size)
    print('=====出现最大/最小换仓周期数的次数')
    print('最大持仓周期数出现的次数', max_size_freq)
    print('最小持仓周期数出现的次数', min_size_freq)
    print('=====最大/最小换仓周期数对应开仓/平仓时间=====')
    print('最大持仓周期数出现的时间(开仓 平仓)', max_size_times)
    print('最小持仓周期数出现的时间(开仓 平仓)', min_size_times)

    # 保存统计信息
    save_path = os.path.join(info_path, f'{stg_name}_子策略表现_{hold_period}_{offset}.csv')
    select_stat.to_csv(save_path, encoding='gbk')
    save_path = os.path.join(info_path, f'{stg_name}_选中次数_{hold_period}_{offset}.csv')
    times.to_csv(save_path, encoding='gbk')


def get_equity_file(base_config_path, base_strategy_dict, hold_period, offset):
    """
        取出对应的资金曲线
    :param base_config_path: 子策略路径
    :param base_strategy_dict: 子策略配置
    :param hold_period: 持仓周期
    :param offset: offset
    :return:
        获取所有对应的资金曲线文件
    """

    # 资金曲线文件路径
    equity_path = os.path.join(base_config_path, 'data/回测结果/')
    # 获取所有的资金曲线文件，只找hold_period和offset对应的资金曲线文件
    all_equity_file = []  # 创建新列表保存资金曲线文件
    for stg in list(base_strategy_dict.keys()):  # 遍历每个策略文件名
        file_name = f'{stg.split("_")[1]}_*_资金曲线_{hold_period}_{offset}.csv'  # 以策略文件名_后的部分开头，之后是SPOT或SWAP，再接着hold_period和offset
        equity_file = glob(equity_path + file_name)  # 将文件夹下所有对应的资金曲线文件找出来
        all_equity_file.extend(equity_file)  # 添加到列表中

    return all_equity_file
